#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "glUtils.h"
#include "log.h"

GLuint LoadShader(GLenum type, const char *shaderSrc) {
    GLuint shader;
    GLint compiled;
    // Create the shader object
    shader = glCreateShader(type);
    if (shader == 0)
        return 0;
    // Load the shader source
    glShaderSource(shader, 1, (const GLchar *const *)&shaderSrc, NULL);
    // Compile the shader
    glCompileShader(shader);
    // Check the compile status
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

    if (!compiled) {
        GLint infoLen = 0;

        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);

        if (infoLen > 1) {
            char *infoLog = malloc(sizeof(char) * infoLen);
            glGetShaderInfoLog(shader, infoLen, NULL, infoLog);
            LOG_ERR("compiling shader: \n%s\n", infoLog);
            free(infoLog);

            LOG_INFO("shader source:\n%s", shaderSrc);
        }
        glDeleteShader(shader);
        return 0;
    }
    return shader;
}

int LoadShaderFile(GLenum type, const char *file) {
    FILE *shaderFile;
    char *buffer;
    shaderFile = fopen(file, "r");
    assert(shaderFile != NULL);
    fseek(shaderFile, 0, SEEK_END);
    size_t fileSize = ftell(shaderFile) + 32;
    rewind(shaderFile);
    buffer = (char *)malloc(fileSize);
    memset(buffer, 0, fileSize);
    fread(buffer, 1, fileSize, shaderFile);
    fclose(shaderFile);
    GLuint shader = LoadShader(type, buffer);
    free(buffer);
    return shader;
}

GLuint LoadShaderProgram(const char *shaderName) {
    GLuint vertexShader;
    GLuint fragmentShader;
    GLuint programObject;
    char fileName[128];
    sprintf(fileName, "shader/%s.vert", shaderName);
    LOG_INFO("@vertex shader: %s\n", fileName);
    vertexShader = LoadShaderFile(GL_VERTEX_SHADER, fileName);
    sprintf(fileName, "shader/%s.frag", shaderName);
    LOG_INFO("@fragment shader: %s\n", fileName);
    fragmentShader = LoadShaderFile(GL_FRAGMENT_SHADER, fileName);
    programObject = glCreateProgram();
    if (programObject == 0)
        return 0;
    glAttachShader(programObject, vertexShader);
    glAttachShader(programObject, fragmentShader);
    return programObject;
}

GLuint InitShader(const char *shaderSrc) {
    LOG_INFO("initilize shader: %s\n", shaderSrc);
    GLuint program = LoadShaderProgram(shaderSrc);
    if (program == 0) {
        return 0;
    }
    glLinkProgram(program);
    GLint linked;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    if (!linked) {
        GLint infoLen = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
        if (infoLen > 1) {
            char *infoLog = (char *)malloc(sizeof(char) * infoLen);

            glGetProgramInfoLog(program, infoLen, NULL, infoLog);
            printf("Error linking program:\n%s\n", infoLog);
            free(infoLog);
        }
        glDeleteProgram(program);
        return 0;
    }
    return program;
}

GLuint InitArrayBuffer(float *data, size_t size) {
    GLuint buff;
    glGenBuffers(1, &buff);
    assert(buff != 0);
    glBindBuffer(GL_ARRAY_BUFFER, buff);
    glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
    return buff;
}

void InitAttrBuffer(GLuint buffer, GLuint attr, size_t num) {
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glVertexAttribPointer(attr, num, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(attr);
}

GLuint InitAttrArrayBuffer(GLuint program, float *data, size_t size, size_t num,
                           const char *attr) {
    GLuint buff = InitArrayBuffer(data, size);
    GLint a_attr = glGetAttribLocation(program, attr);
    assert(a_attr >= 0);
    glVertexAttribPointer(a_attr, num, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(a_attr);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    return buff;
}

GLuint InitElementsBuffer(void *data, size_t size) {
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    GLuint idx_buf;
    glGenBuffers(1, &idx_buf);
    if (!idx_buf) {
        return 0;
    }
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idx_buf);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
    return idx_buf;
}

GLuint InitTexture(GLuint program, SDL_Surface *sur, uint32_t texn,
                   const char *uni) {
    GLint u_sam = glGetUniformLocation(program, uni);
    assert(u_sam >= 0);
    GLuint tex;
    glGenTextures(1, &tex);
    assert(tex != 0);
    glActiveTexture(GL_TEXTURE0 + texn);
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, sur->w, sur->h, 0, GL_RGB,
                 GL_UNSIGNED_BYTE, sur->pixels);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glUniform1i(u_sam, texn);
    return tex;
}

int InitFrameBuffer(gl_fbo *fbo, uint32_t w, uint32_t h) {
    fbo->h = h;
    fbo->w = w;

    glGenTextures(1, &fbo->tex);
    if (!fbo->tex) {
        LOG_ERR("err tex\n");
    }

    glBindTexture(GL_TEXTURE_2D, fbo->tex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE,
                 NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    glGenFramebuffers(1, &fbo->fbo);
    if (!fbo->fbo) {
        LOG_ERR("err fbo\n");
    }

    glBindFramebuffer(GL_FRAMEBUFFER, fbo->fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
                           fbo->tex, 0);

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindTexture(GL_TEXTURE_2D, 0);
    return fbo->fbo;
}
